package org.sean.framework.web.tools;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.sean.framework.context.SpringApplicationContext;
import org.sean.framework.pool.IOTaskExecutor;
import org.sean.framework.util.ObjectUtil;
import org.springframework.boot.actuate.web.mappings.MappingsEndpoint;
import org.springframework.boot.actuate.web.mappings.servlet.DispatcherServletMappingDescription;
import org.springframework.boot.actuate.web.mappings.servlet.DispatcherServletMappingDetails;
import org.springframework.boot.actuate.web.mappings.servlet.RequestMappingConditionsDescription;
import org.springframework.core.Ordered;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * Api 记录
 */
public class ApiFinder extends OncePerRequestFilter implements Ordered {

    private MappingsEndpoint mappingsEndpoint;

    private Set<Api> apis;

    private ApiPoster poster;

    private PathMatcher matcher = new AntPathMatcher();

    public ApiFinder() {
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        IOTaskExecutor.getInstance().execute(() -> {
            if (mappingsEndpoint == null) {
                mappingsEndpoint = SpringApplicationContext.getBean(MappingsEndpoint.class);
            }

            if (poster == null) {
                poster = SpringApplicationContext.getBean(ApiPoster.class);
            }

            if (ObjectUtil.isEmpty(apis)) {
                apis = getApis();
                if (ObjectUtil.isNotEmpty(apis)) {
                    poster.handlerApis(apis);
                }
            }

            if (poster != null && ObjectUtil.isNotEmpty(apis)) {
                try {
                    String uri = request.getRequestURI();
                    String method = request.getMethod();
                    Optional<Api> rawApi = apis.stream()
                            .filter(api -> method.equals(api.getMethod()) && matcher.match(api.getPattern(), uri)).findAny();
                    String url;
                    if (rawApi.isPresent()) {
                        Api api = rawApi.get();
                        url = api.getMethod() + "-" + api.getPattern();
                    } else {
                        url = method + "-" + uri;
                    }
                    poster.handlerUrl(url);
                } catch (Exception e) {
                    // do nothing
                }
            }
        });
        filterChain.doFilter(request, response);
    }

    private Set<Api> getApis() {
        if (mappingsEndpoint != null) {
            Map<String, MappingsEndpoint.ContextMappings> a = mappingsEndpoint.mappings().getContexts();
            Set<Api> apis = new HashSet<>();
            for (Map.Entry<String, MappingsEndpoint.ContextMappings> entry : a.entrySet()) {
                for (Map.Entry<String, Object> mapping : entry.getValue().getMappings().entrySet()) {
                    if ("dispatcherServlets".equals(mapping.getKey())) {
                        ((Map<String, List<DispatcherServletMappingDescription>>) mapping.getValue()).values().forEach(descriptions -> {
                            for (DispatcherServletMappingDescription description : descriptions) {
                                DispatcherServletMappingDetails details = description.getDetails();
                                if (details != null) {
                                    RequestMappingConditionsDescription conditions = description.getDetails().getRequestMappingConditions();
                                    for (RequestMethod method : conditions.getMethods()) {
                                        for (String pattern : conditions.getPatterns()) {
                                            apis.add(new Api(method.name(), pattern));
                                        }
                                    }
                                }
                            }
                        });
                    }
                }
            }
            return apis;
        }
        return Collections.emptySet();
    }

    /**
     * API
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Api {
        String method;
        String pattern;
    }
}
